/*##########################################################################
#    Copyright (C) 2004 by neuron                                          
#    neuron@hollowtube.mine.nu                                             
#                                                                          
#    This program is free software; you can redistribute it and#or modify  
#    it under the terms of the GNU General Public License as published by  
#    the Free Software Foundation; either version 2 of the License, or     
#    (at your option) any later version.                                   
#                                                                          
#    This program is distributed in the hope that it will be useful,       
#    but WITHOUT ANY WARRANTY; without even the implied warranty of        
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
#    GNU General Public License for more details.                          
#                                                                          
#    You should have received a copy of the GNU General Public License     
#    along with this program; if not, write to the                         
#    Free Software Foundation, Inc.,                                       
#    59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             
############################################################################*/


/* 
 * pam_ssh512 by neuron@hollowtube.mine.nu
 * Heavily based on pam_pwdfile.c, I wish I was clever enough to add support for sha in it, unfortunatly I wasn't.
 * pam_pwdfile.c copyright 1999-2003 by Charl P. Botha <cpbotha@ieee.org>
 * and using Copyright (c) 2001-2003 Allan Saddi <allan@saddi.com>'s ssh implementation.
 */
 
/*
  Instructions:
  make install
  edit for example /etc/pam.d/sshd and add:
  auth       required     /lib/security/pam_sha512.so pwdfile /etc/security/pam.sshd.sha
  This will require a sshd login to be validated and ok'ed by ssh, using /etc/security/pam.sshd.sha
  
  Alternatly for something system wide:
  auth       required     /lib/security/pam_sha512.so pwdfile /etc/security/pam.sha
  OR if you have other ways of authenticating aswell (for example ldap/system):
  auth       sufficient     /lib/security/pam_sha512.so pwdfile /etc/security/pam.sha
  
  The password files are easy enough to edit, install hashalot.
  touch your.sha
  echo "$USER:`hashalot -x sha512`" >> /etc/security/your.sha
  
  I've considered changing it to monitoring multiple files in a directory.  Then each user could own their own file, and change password if they wanted to.
  Let me know if anyone wants this.
*/

#include <features.h>
#include <syslog.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/file.h>
#include <unistd.h>

#include <security/pam_appl.h>

#define PAM_SM_AUTH
#include <security/pam_modules.h>

#define HAVE_SHA512

#ifdef HAVE_SHA512
#include "sha512.h"
#endif

#define PWDF_PARAM "pwdfile"
#define FLOCK_PARAM "flock"
#define NODELAY_PARAM "nodelay"
#define PWDFN_LEN 256

#ifdef DEBUG
# define D(a) a;
#else
# define D(a) {}
#endif

/* prototypes */
int converse(pam_handle_t *, int, struct pam_message **, struct pam_response **);
int _set_auth_tok(pam_handle_t *, int, int, const char **);

/* logging function ripped from pam_listfile.c */
static void _pam_log(int err, const char *format, ...) {
    va_list args;
    
    va_start(args, format);
    openlog("pam_sha512", LOG_CONS|LOG_PID, LOG_AUTH);
    vsyslog(err, format, args);
    va_end(args);
    closelog();
}

static int lock_fd(int fd) {
    int delay;
    
    for (delay = 5; delay <= 40; delay *= 2) {
	if (flock(fd, LOCK_SH | LOCK_NB) == -1) {
	    /* failed */
	    if (errno != EWOULDBLOCK) goto failed;
	    sleep(delay);
	}else{
	    return 0;
	}
    }
    if (flock(fd, LOCK_SH | LOCK_NB) != -1) return 0;
    failed:
    return -1;
}

/* this function ripped from pam_unix/support.c */
int converse(	pam_handle_t *pamh,
		int nargs, 
		struct pam_message **message,
		struct pam_response **response	) {
    int retval;
    struct pam_conv *conv;
    
    retval = pam_get_item(	pamh, PAM_CONV,  (const void **) &conv ) ; 
    if ( retval == PAM_SUCCESS ) {
	retval = conv->conv( 	nargs,  
				( const struct pam_message ** ) message, 
				response, 
				conv->appdata_ptr );
    }
    return retval;
}

/* this function ripped from pam_unix/support.c */
int _set_auth_tok(	pam_handle_t *pamh, 
			int flags, int argc, 
			const char **argv	) {
    int	retval;
    char	*p;
    
    struct pam_message msg[1],*pmsg[1];
    struct pam_response *resp;
    
    /* set up conversation call */
    
    pmsg[0] = &msg[0];
    msg[0].msg_style = PAM_PROMPT_ECHO_OFF;
    msg[0].msg = "Password: ";
    resp = NULL;
    
    if ( ( retval = converse( pamh, 1 , pmsg, &resp ) ) != PAM_SUCCESS ) 
	return retval;
    
    if ( resp ) 
    {
	if ( ( flags & PAM_DISALLOW_NULL_AUTHTOK ) && 
	     resp[0].resp == NULL ) 
	{
	    free( resp );
	    return PAM_AUTH_ERR;
	}
	
	p = resp[ 0 ].resp;
	
	/* This could be a memory leak. If resp[0].resp 
	 is malloc()ed, then it has to be free()ed! 
	 -- alex 
	 */
	
	resp[ 0 ].resp = NULL; 		  				  
	
    } 
    else 
	return PAM_CONV_ERR;
    
    free( resp );
    pam_set_item( pamh, PAM_AUTHTOK, p );
    return PAM_SUCCESS;
}


/* puts the crypted password corresponding to user "name" in password,
 * from a file with lines consisting of: name:crypted_password
 * if unsucessful, returns 0
 */
static int fgetpwnam(FILE *stream, const char *name, char *password) {
    char tempLine[256], *tpointer, *curname, *curpass, *fgr;
    int loopdone, pwdfound;
    int len;
    
    /* go to beginning of file */
    rewind(stream);
    /* some control variables */
    loopdone = pwdfound = 0;
    /* fgets should do this, but we make sure */
    tempLine[255] = '\0';
    /* iterate through lines in file, until end of file */
    do {
	/* get the current line */
	fgr = fgets(tempLine,255,stream);
	/* if it's valid, go on */
	if ( fgr != NULL) {
	    /* first get the username out */
	    tpointer = tempLine;
	    curname = strsep(&tpointer,":");
	    /* check to see if it's the right one */
	    if (strcmp(curname,name)==0) {
		/* at least we know our loop is done */
		loopdone = 1;
		/* remove possible trailing newline */
		len = strlen(tpointer);
		if (tpointer[len - 1] == '\n')
		    tpointer[len - 1] = '\0';
		/* get the password and put it in its place */
		curpass = strsep(&tpointer,":");
		if (curpass != NULL) {
		    /* we use bigcrypt pwd len, as this is just a safe maximum */
		    strncpy(password,curpass,(SHA512_HASH_SIZE*2)+1);
		    pwdfound = 1;
		} /* if (curpass... */
	    } /* if (strcmp(curname... */
	} /* if (tempLine... */
    } while (fgr != NULL);
    return pwdfound;
}

/* expected hook for auth service */
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
				   int argc, const char **argv) {
    int retval, pcnt, pwdfilename_found;
    const char *name;
    char *password;
    char pwdfilename[PWDFN_LEN];
    char stored_crypted_password[(SHA512_HASH_SIZE*2)+1];
    char crypted_password[(SHA512_HASH_SIZE*2)+1];
    FILE *pwdfile;
    int use_flock = 0;
    int use_delay = 1;
    int temp_result = 0;
  SHA512Context sha512;
  uint8_t hash[SHA512_HASH_SIZE];
  char *passwd_loc;
  int i;

    
    /* we require the pwdfile switch and argument to be present, else we don't work */
    /* pcnt is the parameter counter variable for iterating through argv */
    pcnt = pwdfilename_found = 0;
    do {
	/* see if the current parameter looks like "pwdfile" */
	if (strcmp(argv[pcnt],PWDF_PARAM)==0) {
	    /* if argv is long enough, grab the subsequent parameter */
	    if (pcnt+1 < argc) {
		/* make sure we can't overflow */
		strncpy(pwdfilename,argv[++pcnt],PWDFN_LEN);
		/* indicate that we've found it */
		pwdfilename_found = 1;
	    }
	    /* also check for "pwdfile=blah" */
	} else if (strncmp(argv[pcnt],PWDF_PARAM "=",sizeof(PWDF_PARAM "=")-1)==0) {
	    /* make sure we can't overflow */
	    strncpy(pwdfilename,argv[pcnt]+sizeof(PWDF_PARAM),PWDFN_LEN);
	    /* indicate that we've found it */
	    pwdfilename_found = 1;
	} else if (strcmp(argv[pcnt],FLOCK_PARAM)==0) {
	    /* we have a "flock" parameter */
	    use_flock = 1;
	} else if (strcmp(argv[pcnt],"no" FLOCK_PARAM)==0) {
	    /* or a "noflock" parameter */
	    use_flock = 0;
	} else if (strcmp(argv[pcnt],NODELAY_PARAM)==0) {
	    /* no delay on authentication failure */
	    use_delay = 0;
	}
	
    } while (++pcnt < argc);
    
#ifdef HAVE_PAM_FAIL_DELAY
    if (use_delay) {
	D(("setting delay"));
	(void) pam_fail_delay(pamh, 2000000);   /* 2 sec delay for on failure */
    }
#endif
    
    /* for some or other reason, the password file wasn't specified */
    if (!pwdfilename_found) {
	_pam_log(LOG_ERR,"password file name not specified");
	return PAM_AUTHINFO_UNAVAIL;
    }
    
    /* DEBUG */
    D(_pam_log(LOG_ERR, "password filename extracted"));
    
    /* now try to open the password file */
    if ((pwdfile=fopen(pwdfilename,"r"))==NULL) {
	_pam_log(LOG_ERR,"couldn't open password file %s",pwdfilename);
	return PAM_AUTHINFO_UNAVAIL;
    }
    
    /* set a lock on the password file */
    if (use_flock && lock_fd(fileno(pwdfile)) == -1) {
	_pam_log(LOG_ERR,"couldn't lock password file %s",pwdfilename);
	return PAM_AUTHINFO_UNAVAIL;
    }
    
    /* get user name */
    if ((retval = pam_get_user(pamh,&name,"login: ")) != PAM_SUCCESS) {
	_pam_log(LOG_ERR, "username not found");
	fclose(pwdfile);
	return retval;
    }
    
    
    /* DEBUG */
    D(_pam_log(LOG_ERR,"username is %s", name));
    
    /* get password - code from pam_unix_auth.c */
    pam_get_item(pamh, PAM_AUTHTOK, (void *)&password);
    if (!password) {
	retval = _set_auth_tok(pamh, flags, argc, argv);
	if (retval!=PAM_SUCCESS) {
	    fclose(pwdfile);
	    return retval;
	}
    }
    pam_get_item(pamh, PAM_AUTHTOK, (void *)&password);
    
    if ((retval = pam_get_item(pamh, PAM_AUTHTOK, (void *)&password)) != PAM_SUCCESS) {
	_pam_log(LOG_ERR, "auth token not found");
	fclose(pwdfile);
	return retval;
    }
    
    /* DEBUG */
    /*D(_pam_log(LOG_ERR,"got password from user", password));*/
    
    /* now crypt password and compare to the user entry in the password file */
    /* first make sure password is long enough -- may I do this? */
    if (strlen(password)<2 || password==NULL) {
	_pam_log(LOG_ERR,"password too short or NULL");
	fclose(pwdfile);
	return PAM_AUTH_ERR;
    }
    
    /* get the crypted password corresponding to this user */
    if (!fgetpwnam(pwdfile, name, stored_crypted_password)) {
	_pam_log(LOG_ERR,"user not found in password database");
	fclose(pwdfile);
	return PAM_AUTHINFO_UNAVAIL;
    }
    
    /* DEBUG */
    D(_pam_log(LOG_ERR,"got crypted password == '%s'", stored_crypted_password));
    
    
    temp_result = 0;
    
    
  /* SHA512 on password */
  SHA512Init (&sha512);
  SHA512Update (&sha512, password, strlen(password));
  SHA512Final (&sha512, hash);
  
  for (i = 0; i < SHA512_HASH_SIZE; i++) {
    passwd_loc=&crypted_password[(i*2)];
    sprintf(passwd_loc,"%02x", hash[i]);
  }
  
	if (strcmp(crypted_password, stored_crypted_password) == 0)
	{
	    temp_result = 1;
	}
  
	
    /* DEBUG */
    D(_pam_log(LOG_ERR,"user password crypted is '%s'", crypted_password));
    
    /* if things don't match up, complain */
    if (!temp_result) 
    {
	_pam_log(LOG_ERR,"wrong password for user %s",name);
	fclose(pwdfile);
	return PAM_AUTH_ERR;
    }
    
    /* DEBUG */
    D(_pam_log(LOG_ERR,"passwords match"));
    
    /* we've gotten here, i.e. authentication was sucessful! */
    fclose(pwdfile);
    return PAM_SUCCESS;
}

/* another expected hook */
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, 
			      int argc, const char **argv)
{
    return PAM_SUCCESS;
}

#ifdef PAM_STATIC
struct pam_module _pam_listfile_modstruct = {
    "pam_sha512",
	pam_sm_authenticate,
	pam_sm_setcred,
	NULL,
	NULL,
	NULL,
	NULL,
};
#endif
